﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading;
using Pfz.Extensions;
using Pfz.Extensions.MonitorLockExtensions;
using Pfz.RemoteGaming.Internal;
using Pfz.Remoting;
using Pfz.Remoting.Udp;
using Pfz.Serialization;
using Pfz.Threading;

namespace Pfz.RemoteGaming
{
	/// <summary>
	/// Class created by the RemoteGameListener when a new connection is created.
	/// </summary>
	public class RemoteGameParticipant:
		IAdvancedDisposable
	{
		#region Fields
			private readonly object _changeRoomLock = new object();
			private object _roomLock;
			private bool _wasDisposed;

			internal Assembly[] _commonAssemblies;
			internal bool _areNotificationsEnabled = true;

			private Dictionary<long, RemoteGameComponent> _privateComponents = new Dictionary<long, RemoteGameComponent>();

			internal HashSet<RemoteGameComponent> _addedComponents = new HashSet<RemoteGameComponent>();
			internal HashSet<long> _removedComponents = new HashSet<long>();
			internal Dictionary<long, Dictionary<int, object>> _componentChanges = new Dictionary<long, Dictionary<int, object>>();
			internal ManagedAutoResetEvent _event = new ManagedAutoResetEvent();
			private List<KeyValuePair<long, object[]>> _volatileValues = new List<KeyValuePair<long, object[]>>();
			internal RemoteGameListener _listener;
		#endregion

		#region Destructor
			/// <summary>
			/// Calls Dispose(false);
			/// </summary>
			~RemoteGameParticipant()
			{
				Dispose(false);
			}
		#endregion
		#region Dispose
			#region Dispose()
				/// <summary>
				/// Calls Dispose(true);
				/// </summary>
				public void Dispose()
				{
					Dispose(true);
					GC.SuppressFinalize(this);
				}
			#endregion
			#region Dispose(bool disposing)
				/// <summary>
				/// Releases all resources used by this participant.
				/// </summary>
				protected void Dispose(bool disposing)
				{
					if (disposing)
					{
						var readerWriterLock = _changeRoomLock;
						if (readerWriterLock == null)
							return;

						lock(_changeRoomLock)
						{
							if (_wasDisposed)
								return;

							_wasDisposed = true;
							_ClearPrivateObjects();
							_privateComponents = null;
							_room = null;
						}

						var waitingRequests = _waitingRequests;
						if (waitingRequests != null)
						{
							_waitingRequests = null;
							foreach(var request in waitingRequests.Values)
								request(null);
						}

						var signal = _event;
						if (signal != null)
							signal.Set();
					}
				}
			#endregion
		#endregion

		#region Properties
			#region WasDisposed
				/// <summary>
				/// Gets a value indicating if this participant was disposed.
				/// </summary>
				public bool WasDisposed
				{
					get
					{
						return _wasDisposed;
					}
				}
			#endregion

			#region Connection
				/// <summary>
				/// Gets the Connection bound to this participant.
				/// </summary>
				public IConnection Connection { get; internal set; }
			#endregion
			#region ParticipantId
				/// <summary>
				/// Gets the Id attributed to this Participant. Changing from room will change this value.
				/// </summary>
				public long ParticipantId { get; private set; }
			#endregion
			#region Room
				private RemoteGameRoom _room;
				/// <summary>
				/// Gets or sets the room to which this client is bound.
				/// </summary>
				public RemoteGameRoom Room
				{	
					get
					{
						return _room;
					}
					set
					{
						if (value == null)
							throw new ArgumentException("Setting the Participant room to null is not acceptable. Create an empty room if you need.");

						lock(_changeRoomLock)
						{
							if (_wasDisposed)
								throw new RemoteGameObjectDisposedException(GetType().FullName);

							if (value == _room)
								return;

							_ClearPrivateObjects();

							_room = value;
							_roomLock = value._RoomLock;
							ParticipantId = Interlocked.Increment(ref RemoteGameRoom._participantIdGenerator);
							lock(_roomLock)
							{
								if (value.WasDisposed)
									throw new RemoteGameObjectDisposedException("New Room");

								value._participants.Add(this);

								if (value._components.Count > 0)
								{
									foreach(var component in value._components.Values)
									{
										Dictionary<int, object> copy;

										component._componentLock.EnterReadLock();
										try
										{
											if (component._wasDisposed || (!component.IsPublic && component.Owner != this))
												continue;

											copy = new Dictionary<int, object>(component._values);
										}
										finally
										{
											component._componentLock.ExitReadLock();
										}

										_addedComponents.Add(component);
										_componentChanges[component._id] = copy;
									}
								}

								if (_areNotificationsEnabled && value._started)
									_event.Set();
							}
						}
					}
				}
			#endregion
		#endregion
		#region Methods
			#region _ApplyClientChanges
				private List<KeyValuePair<long, object>> _requestResults = new List<KeyValuePair<long, object>>();
				internal void _ApplyClientChanges(_ClientChangesInfo changes)
				{
					using(var readLock = _changeRoomLock.Lock())
					{
						if (ParticipantId != changes._clientId)
							return;

						if (_wasDisposed)
							return;

						var lockObject = _roomLock;
						if (lockObject == null)
							return;

						lock(lockObject)
						{
							var room = _room;

							if (room.WasDisposed)
								return;

							readLock.Dispose();
							// we can apply all our changes to this room, even if another thread just decides to change
							// us from room. Releasing this readLock avoids possible dead-lock (or recursion exceptions)
							// if one of our requests changes the room. Also the room is kept ok as we keep the lock
							// over it.

							var request = changes._request;
							if (request != null)
							{
								object result = null;

								Func<RemoteGameParticipant, RemoteGameRequest, object> action;
								if (room._registeredActions.TryGetValue(request.GetType(), out action))
									result = action(this, request);

								if (request._expectedResultId != 0)
								{
									var pair = new KeyValuePair<long, object>(request._expectedResultId, result);
									_requestResults.Add(pair);
								}

								if (_requestResults.Count > 0)
									_event.Set();
							}

							var componentChanges = changes._changes;
							_ApplyComponentChanges(room, componentChanges, true);

							var requestResults = changes._requestResults;
							if (requestResults != null)
							{
								foreach(var pair in requestResults)
								{
									var requestId = pair.Key;
									var action = _waitingRequests[requestId];
									_waitingRequests.Remove(requestId);
									action(pair.Value);
								}
							}
						}
					}
				}
			#endregion
			#region _ApplyComponentChanges
				private long _lastProcessedChangesVersion;
				private void _ApplyComponentChanges(RemoteGameRoom room, _ComponentChanges componentChanges, bool applyIfRepeated)
				{
					if (componentChanges == null)
						return;

					long version = componentChanges._version;
					if (version <= _lastProcessedChangesVersion)
						return;

					if (componentChanges._participantId != ParticipantId)
						return;

					foreach(var futureChange in _futureChanges)
					{
						if (version == futureChange._version)
						{
							if (applyIfRepeated)
								break;

							return;
						}
					}

					_futureChanges.Add(componentChanges);
					_ApplyFutureChangesImmediately(room);
				}
			#endregion
			#region _ApplyComponentChangesWithLock
				internal BinarySerializer _udpSerializer;
				internal void _ApplyComponentChangesWithLock(_ComponentChanges changes)
				{
					using(var readLock = _changeRoomLock.Lock())
					{
						if (ParticipantId != changes._participantId)
							return;

						if (_wasDisposed)
							throw new RemoteGameObjectDisposedException(GetType().FullName);

						var lockObject = _roomLock;
						if (lockObject == null)
							return;

						lock(lockObject)
						{
							var room = _room;

							if (room.WasDisposed)
								return;

							readLock.Dispose();

							_ApplyComponentChanges(room, changes, false);
						}
					}
				}
			#endregion
			#region _ApplyFutureChangesImmediately
				private List<_ComponentChanges> _futureChanges = new List<_ComponentChanges>();
				private void _ApplyFutureChangesImmediately(RemoteGameRoom room)
				{
					if (_futureChanges.Count > 1)
						_futureChanges.Sort((a, b) => a._version.CompareTo(b._version));

					bool ok = true;
					foreach(var componentChanges in _futureChanges)
					{
						foreach (var pair in componentChanges._changes)
						{
							long componentId = pair.Key;
							var component = _privateComponents.GetValueOrDefault(componentId);
							if (component == null)
								component = room._components[componentId];

							if (component.Owner != this)
							{
								ok = false;
								continue;
							}

							component._componentLock.EnterWriteLock();
							try
							{
								if (!component._wasDisposed)
								{
									RemoteGameProperty._propertiesLock.EnterReadLock();
									try
									{
										var valuesDictionary = component._values;
										var modificationValues = pair.Value;
										foreach (var modificationPair in modificationValues)
										{
											var key = modificationPair.Key;
											var value = modificationPair.Value;

											if (key < 0 || key >= RemoteGameProperty._properties.Count)
												throw new RemoteGameException("Client is trying to set an inexisting property.");

											var propertyInfo = RemoteGameProperty._properties[key];
											if (!propertyInfo._isClientProperty)
												throw new RemoteGameException("Client is trying to set a server property.");

											if (!component.GetType().IsSubclassOf(propertyInfo._declaringType))
												throw new RemoteGameException("The client is trying to set a property of another component.");

											if (value == null)
												valuesDictionary.Remove(key);
											else
												valuesDictionary[key] = value;
										}
									}
									finally
									{
										RemoteGameProperty._propertiesLock.ExitReadLock();
									}
								}
							}
							finally
							{
								component._componentLock.ExitWriteLock();
							}
						}

						if (ok && componentChanges._version == _lastProcessedChangesVersion+1)
							_lastProcessedChangesVersion++;
					}

					int count = _futureChanges.Count;
					int countToDelete = count;
					for(int i=0; i<count; i++)
					{
						if (_futureChanges[i]._version > _lastProcessedChangesVersion)
						{
							countToDelete = i;
							break;
						}
					}

					if (countToDelete > 0)
						_futureChanges.RemoveRange(0, countToDelete);
				}
			#endregion

			#region _ClearPrivateObjects
				private void _ClearPrivateObjects()
				{
					var lockObject = _roomLock;
					if (lockObject == null)
						return;

					lock(lockObject)
					{
						var components = _privateComponents;

						foreach(var component in components.Values)
						{
							component._owner = null;
							component.Dispose();
						}

						components.Clear();

						if (!_room.WasDisposed)
						{
							_room._participants.Remove(this);

							foreach(var component in _room._addedComponents)
								if (!component.WasDisposed && component.Owner == this)
									component.OnParticipantDisconnected();
							
							foreach(var component in _room._components.Values)
								if (!component.WasDisposed && component.Owner == this)
									component.OnParticipantDisconnected();
						}
					}
				}
			#endregion
			#region _RemoveComponents
				internal void _RemoveComponent(RemoteGameComponent component)
				{
					lock(_changeRoomLock)
					{
						if (_wasDisposed)
							return;

						lock(_roomLock)
						{
							if (!_addedComponents.Remove(component))
								_removedComponents.Add(component._id);

							_privateComponents.Remove(component._id);
						}
					}
				}
			#endregion
			#region _StoreChanges
				internal void _StoreChanges(List<KeyValuePair<RemoteGameComponent, KeyValuePair<int, object>[]>> componentChanges, IEnumerable<RemoteGameComponent> addedComponents, IEnumerable<RemoteGameComponent> removedComponents, List<KeyValuePair<long, object[]>> volatileValues)
				{
					lock(_changeRoomLock)
					{
						if (_wasDisposed)
							return;

						var lockObject = _roomLock;
						if (lockObject == null)
							return;

						lock(lockObject)
						{
							foreach(var component in addedComponents)
							{
								if (component._wasDisposed)
									continue;

								_addedComponents.Add(component);
							}

							foreach(var component in removedComponents)
							{
								if (!_addedComponents.Remove(component))
									_removedComponents.Add(component._id);

								_componentChanges.Remove(component._id);
							}

							foreach(var pair1 in componentChanges)
							{
								var component = pair1.Key;

								if (component._wasDisposed)
									continue;

								var changes = pair1.Value;
								_AddChanges(component, changes);
							}

							_volatileValues.Clear();
							_volatileValues.AddRange(volatileValues);
							foreach(var component in _privateComponents.Values)
							{
								var changes = component._GetChanges();
								if (changes != null)
									_AddChanges(component, changes);

								component._AddVolatileValues(_volatileValues);
							}

							if (_addedComponents.Count > 0 || _removedComponents.Count > 0 || _componentChanges.Count > 0 || _volatileValues.Count > 0)
								_event.Set();
						}
					}
				}
			#endregion
			#region _AddChanges
				private void _AddChanges(RemoteGameComponent component, KeyValuePair<int, object>[] changes)
				{
					if (changes != null)
					{
						var changeDictionary = _componentChanges.GetOrCreateValue(component._id);
						foreach (var modificationPair in changes)
						{
							int propertyId = modificationPair.Key;
							var value = modificationPair.Value;

							changeDictionary[propertyId] = value;
						}
					}
				}
			#endregion
			#region _GetServerChanges
				private long _componentChangesVersion;
				internal _ServerChangesInfo _GetServerChanges()
				{
					try
					{
						while(!_wasDisposed)
						{
							_event.WaitOne();

							lock(_changeRoomLock)
							{
								if (_wasDisposed)
									return null;

								if (!_areNotificationsEnabled)
									continue;

								var lockObject = _roomLock;
								if (lockObject == null)
									continue;

								lock(lockObject)
								{
									_futureChanges.Clear();

									var room = _room;
									if (room.WasDisposed)
										continue;

									int count = _addedComponents.Count;
									KeyValuePair<Type, long>[] addedComponents = null;
									if (count > 0)
									{
										addedComponents = new KeyValuePair<Type, long>[count];
										int index = -1;
										foreach(var component in _addedComponents)
										{
											index++;

											var pair = new KeyValuePair<Type, long>(_GetCommonType(component), component._id);
											addedComponents[index] = pair;
										}
										_addedComponents.Clear();
									}

									int requestCount;
									lock(_requests)
										requestCount = _requests.Count;

									int componentChangeCount = _componentChanges.Count;
									if (addedComponents == null && _removedComponents.Count == 0 && componentChangeCount == 0 && _requestResults.Count == 0 && requestCount == 0 && _volatileValues.Count == 0)
										continue;

									RemoteGameRequest request = null;
									if (requestCount > 0)
									{
										if (addedComponents != null)
											_event.Set();
										else
										{
											lock(_requests)
												request = _requests.Dequeue();

											if (requestCount > 1)
												_event.Set();
										}
									}

									KeyValuePair<long, object>[] requestResults = null;
									if (_requestResults.Count > 0)
									{
										requestResults = _requestResults.ToArray();
										_requestResults.Clear();
									}

									long[] removedComponents = null;
									if (_removedComponents.Count > 0)
									{
										removedComponents = _removedComponents.ToArray();
										_removedComponents.Clear();
									}

									_VolatileValues volatileValues = null;
									if (_volatileValues.Count > 0)
										volatileValues = new _VolatileValues(_volatileValues.ToArray());

									_ComponentChanges componentChanges = null;
									if (componentChangeCount > 0)
									{
										var changes = new KeyValuePair<long, KeyValuePair<int, object>[]>[componentChangeCount];
										int componentIndex = -1;
										foreach(var pair in _componentChanges)
										{
											componentIndex++;

											long id = pair.Key;
											var value = pair.Value.ToArray();
											var pair2 = new KeyValuePair<long, KeyValuePair<int, object>[]>(id, value);
											changes[componentIndex] = pair2;
										}

										_componentChangesVersion++;
										componentChanges = new _ComponentChanges(_componentChangesVersion, ParticipantId, changes);
										_componentChanges.Clear();
									}

									if (addedComponents == null && (componentChanges != null || volatileValues != null))
									{
										var udpConnection = Connection as IUdpConnection;
										if (udpConnection != null)
										{
											var serializer = _udpSerializer;
											if (serializer != null)
											{
												using(var stream = new MemoryStream())
												{
													if (componentChanges != null)
														serializer.Serialize(stream, componentChanges);

													if (volatileValues != null)
														serializer.Serialize(stream, volatileValues);

													if (stream.Length <= 1024)
													{
														volatileValues = null;

														var guaranteedConnection = udpConnection as IGuaranteedUdpConnection;
														if (guaranteedConnection != null && componentChanges != null)
														{
															guaranteedConnection.GuaranteedSend(stream.GetBuffer(), 0, (int)stream.Length);
															componentChanges = null;
														}
														else
															udpConnection.UdpSend(stream.GetBuffer(), 0, (int)stream.Length);
													}
												}
											}
										}
									}

									if 
									(
										addedComponents == null && 
										componentChanges == null && 
										removedComponents == null && 
										request == null && 
										requestResults == null && 
										volatileValues == null
									)
									{
										continue;
									}

									var result = new _ServerChangesInfo();
									result._participantId = ParticipantId;
									result._addedComponents = addedComponents;
									result._componentChanges = componentChanges;
									result._removedComponents = removedComponents;
									result._request = request;
									result._requestResults = requestResults;
									result._volatileValues = volatileValues;
									return result;
								}
							}
						}
					}
					finally
					{
						if (_wasDisposed)
							Disposer.Dispose(ref _event);
					}

					return null;
				}
			#endregion
			#region _Run
				internal void _Run()
				{
					var connection = Connection;

					try
					{
						using(var stream = connection.Stream)
						{
							var binarySerializer = new BinarySerializer();
							binarySerializer.AddRecommendedDefaults();
							binarySerializer.AddDefaultTypeRecursive(typeof(_ServerChangesInfo));
							binarySerializer.AddDefaultTypeRecursive(typeof(_ClientChangesInfo));

							var initializationData = _listener._GetClientInitializationData();

							UnlimitedThreadPool.Run(_Sender, Tuple.Create(stream, initializationData));
							initializationData.AddDefaultTypes(binarySerializer);

							var context = new _RemoteGameContext();
							binarySerializer.Context = new StreamingContext(StreamingContextStates.All, context);
							while(!WasDisposed)
							{
								object clientChanges;
								try
								{
									var room = Room;
									if (room != null)
									{
										context._lock = room._RoomLock;
										context._components = room._components;
										context._ownerId = ParticipantId;
									}

									clientChanges = (_ClientChangesInfo)binarySerializer.Deserialize(stream);
								}
								catch
								{
									return;
								}

								_ApplyClientChanges((_ClientChangesInfo)clientChanges);
							}
						}
					}
					catch(Exception exception2)
					{
						if (!connection.WasDisposed)
							_listener._OnException(exception2);
						// when applying client changes the server should only disconnect the actual client, not the listener.
					}
				}
			#endregion
			#region _UdpReceiver
				private void _UdpReceiver(_InitializationData initializationData)
				{
					var connection = Connection;
					if (connection == null)
						return;

					try
					{
						var udpConnection = Connection as IUdpConnection;
						if (udpConnection == null)
							return;

						var udpDeserializer = new BinarySerializer();
						udpDeserializer.AddRecommendedDefaults();
						udpDeserializer.AddDefaultTypeRecursive(typeof(_ComponentChanges));
						udpDeserializer.AddDefaultTypeRecursive(typeof(_VolatileValues));
						initializationData.AddDefaultTypes(udpDeserializer);

						while(true)
						{
							var received = udpConnection.UdpReceive();
							if (received == null)
								return;

							object deserialized;
							using(var stream2 = new MemoryStream(received, false))
							{
								stream2.Position = udpConnection.UdpHeaderSize;

								try
								{
									deserialized = udpDeserializer.Deserialize(stream2);
								}
								catch
								{
									return;
								}
							}

							var changes = (_ComponentChanges)deserialized;
							_ApplyComponentChangesWithLock(changes);
						}
					}
					catch(Exception exception)
					{
						if (!connection.WasDisposed)
						{
							var room = Room;
							if (room != null && room.WasDisposed)
								return;

							if (!_listener._OnException(exception))
								throw;
						}
					}
					finally
					{
						Dispose();
					}
				}
			#endregion
			#region _Sender
				private void _Sender(Tuple<Stream, _InitializationData> parameters)
				{
					var stream = parameters.Item1;
					var initializationData = parameters.Item2;
					var connection = Connection;
					if (connection == null)
						return;

					try
					{
						var binarySerializer = new BinarySerializer();
						binarySerializer.AddRecommendedDefaults();
						binarySerializer.AddDefaultTypeRecursive(typeof(_ServerChangesInfo));
						binarySerializer.AddDefaultTypeRecursive(typeof(_ClientChangesInfo));

						try
						{
							binarySerializer.Serialize(stream, initializationData);
							stream.Flush();
						}
						catch
						{
							return;
						}

						var udpConnection = Connection as IUdpConnection;
						if (udpConnection != null)
						{
							var unguaranteedSerializer = new BinarySerializer();
							unguaranteedSerializer.AddRecommendedDefaults();
							unguaranteedSerializer.AddDefaultTypeRecursive(typeof(_ComponentChanges));
							unguaranteedSerializer.AddDefaultTypeRecursive(typeof(_VolatileValues));
							initializationData.AddDefaultTypes(unguaranteedSerializer);
							_udpSerializer = unguaranteedSerializer;

							UnlimitedThreadPool.Run(_UdpReceiver, initializationData);
						}

						initializationData.AddDefaultTypes(binarySerializer);

						while(true)
						{
							var serverChanges = _GetServerChanges();
							if (serverChanges == null)
								return;

							try
							{
								binarySerializer.Serialize(stream, serverChanges);
								stream.Flush();
							}
							catch
							{
								return;
							}
						}
					}
					catch(Exception exception)
					{
						if (!connection.WasDisposed)
						{
							var room = Room;
							if (room != null && room.WasDisposed)
								return;

							if (!_listener._OnException(exception))
								throw;
						}
					}
					finally
					{
						Dispose();
					}
				}
			#endregion

			#region _GetCommonType
				private Type _GetCommonType(RemoteGameComponent component)
				{
					var type = component.GetType();
					while(type != null)
					{
						var assembly = type.Assembly;

						foreach(var commonAssembly in _commonAssemblies)
							if (assembly == commonAssembly)
								return type;

						type = type.BaseType;
					}

					throw new RemoteGameException("Can't find the common type for " + component.GetType().FullName);
				}
			#endregion

			#region BeginUpdates
				/// <summary>
				/// Use this to do a lot of updates together, so the notifications will only be
				/// sent when ending all updates.
				/// Use it in a using clause, so notifications are restored at the end.
				/// </summary>
				public IDisposable BeginUpdates()
				{
					return new _NotificationsDisablerForServer(this);
				}
			#endregion
			#region CreateComponent
				#region CreateComponent()
					/// <summary>
					/// Creates a private component owned by this participant.
					/// </summary>
					public T CreateComponent<T>()
					where
						T: RemoteGameComponent
					{
						var result = (T)_AssemblyGenerator.GetConstructor(typeof(T)).Invoke(null);
						try
						{
							lock(_changeRoomLock)
							{
								if (_wasDisposed)
									throw new RemoteGameObjectDisposedException(GetType().FullName);
					
								var lockObject = _roomLock;
								if (lockObject == null)
									throw new RemoteGameException("Components can only be created after joining a room.");

								lock(lockObject)
								{
									result._roomLock = lockObject;
									result._room = _room;
									long id = Interlocked.Increment(ref RemoteGameRoom._idGenerator);
									result._id = id;
									_addedComponents.Add(result);
									_privateComponents.Add(id, result);
									result.Owner = this;
									result.OnInitialize();
								}
							}
						}
						catch
						{
							result.Dispose();
							throw;
						}

						return result;
					}
				#endregion
				#region CreateComponent(bool isPublic)
					/// <summary>
					/// Creates a new component owned by this room, but lets you decide if it is public or not.
					/// </summary>
					public T CreateComponent<T>(bool isPublic)
					where
						T: RemoteGameComponent
					{
						if (isPublic)
						{
							var room = _room;
							if (room == null)
								throw new RemoteGameException("Components can only be created after joining a room.");

							return room._CreateComponent<T>(this);
						}

						return CreateComponent<T>();
					}
				#endregion
			#endregion
			#region GetPrivateComponents
				/// <summary>
				/// Gets all the private components of this participant.
				/// </summary>
				/// <returns>An array with the private components or null if this participant was disposed.</returns>
				public RemoteGameComponent[] GetPrivateComponents()
				{
					lock(_changeRoomLock)
					{
						if (_wasDisposed)
							return null;

						return _privateComponents.Values.ToArray();
					}
				}
			#endregion
			#region Request
				private readonly Queue<RemoteGameRequest> _requests = new Queue<RemoteGameRequest>();
				private Dictionary<long, Action<object>> _waitingRequests = new Dictionary<long, Action<object>>();
				private long _expectedResultId;
				/// <summary>
				/// Requests an action from the client.
				/// Optionally sets a result callback. Note that the result may be null if the connection
				/// is closed, even if null is not a valid result value.
				/// </summary>
				public void Request(RemoteGameRequest request, Action<object> onResult=null)
				{
					if (request == null)
						throw new ArgumentNullException("request");

					lock(_changeRoomLock)
					{
						if (_wasDisposed)
							throw new RemoteGameObjectDisposedException(GetType().FullName);

						lock(_requests)
						{
							_requests.Enqueue(request);

							if (onResult != null)
							{
								_expectedResultId++;
								_waitingRequests.Add(_expectedResultId, onResult);
								request._expectedResultId = _expectedResultId;
							}
						}

						_event.Set();
					}
				}
			#endregion
			#region SyncRequest
				/// <summary>
				/// Requests something to the client and keeps waiting until it responds.
				/// Do not call this from an animation, only from secondary threads, or you will cause a dead-lock.
				/// </summary>
				public object SyncRequest(RemoteGameRequest request)
				{
					object result = null;
					using(var waitEvent = new ManagedManualResetEvent(false))
					{
						Request
						(
							request, 
							(localResult) =>
							{
								result = localResult;
								waitEvent.Set();
							}
						);
				
						waitEvent.WaitOne();
						return result;
					}
				}
			#endregion
		#endregion
	}
}
